package LDraw.Files; import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.Dictionary; import java.util.HashMap; import java.util.Hashtable; import java.util.TreeSet; import javax.media.opengl.GL2; import Command.LDrawLSynthDirective; import Command.LDrawPart; import Common.Box2; import Common.Box3; import Common.Matrix4; import Common.Ray3; import Common.Vector2f; import Common.Vector3f; import LDraw.Support.DispatchGroup; import LDraw.Support.ILDrawObservable; //============================================================================== // //File: LDrawFile.h // //Purpose: Represents an LDraw file, composed of one or more models. // In Bricksmith, each file is interpreted as a Multi-Part Document // having multiple submodels. Only LDrawMPDModels can be contained // in the file's subdirective array. However, when the document is // written out, the MPD commands are stripped if there is only // one model in the file. // //Threading: An LDrawFile can be drawn by multiple threads simultaneously. // What we must not do is edit while drawing or draw while editing. // To prevent such unpleasantries, bracket any editing to this File // (or any descendant directives) with calls to -lockForEditing and // -unlockEditor. // //Created by Allen Smith on 2/19/05. //Copyright (c) 2005. All rights reserved. //============================================================================== import LDraw.Support.LDrawDirective; import LDraw.Support.LDrawUtilities; import LDraw.Support.PartReport; import LDraw.Support.Range; import LDraw.Support.type.MessageT; import Notification.NotificationCenter; import Notification.NotificationMessageT; import Renderer.ILDrawCollector; import Renderer.ILDrawRenderer; import Window.MOCBuilder; /** * @Class LDrawFile * * @Purpose Represents an LDraw file, composed of one or more models. In * Bricksmith, each file is interpreted as a Multi-Part Document having * multiple submodels. Only LDrawMPDModels can be contained in the * file's subdirective array. However, when the document is written * out, the MPD commands are stripped if there is only one model in the * file. * @Represent LDrawFile.(h, m) of Bricksmith * * @author funface2 * @since 2014-03-18 * */ public class LDrawFile extends LDrawContainer { //public static final String LDrawFileActiveModelDidChangeNotification = "LDrawFileActiveModelDidChangeNotification"; /** * @uml.property name="nameModelDict" * @uml.associationEnd * qualifier="toLowerCase:java.lang.String LDraw.Files.LDrawMPDModel" */ Dictionary<String, LDrawMPDModel> nameModelDict; /** * @uml.property name="activeModel" * @uml.associationEnd */ LDrawMPDModel activeModel; /** * @uml.property name="filePath" */ String filePath; // where this file came from on disk. public static LDrawFile newEditableFile() { LDrawFile file = new LDrawFile(); LDrawMPDModel firstModel = LDrawMPDModel.model(); // Fill it with one empty model. file.addSubmodel(firstModel); file.setActiveModel(firstModel); return file; } private LDrawFile() { init(); } public static LDrawFile newEmptyFile(){ LDrawFile file = new LDrawFile(); return file; } public static LDrawFile fileFromContentsAtPath(String path) { LDrawFile file = null; String fileContents = LDrawUtilities.stringFromFile(path); if (fileContents != null && !fileContents.equals("")) { file = parseFromFileContents(fileContents); file.setPath(path); } return file; }// end fileFromContentsAtPath: // ---------- parseFromFileContents: // // Purpose: Reads a file out of the raw file contents. // // ------------------------------------------------------------------------------ private static LDrawFile parseFromFileContents(String fileContents) { String[] lines = fileContents.replaceAll("\r", "").split("\n"); ArrayList<String> lineArray = new ArrayList<String>(); for (String line : lines) lineArray.add(line); LDrawFile file = new LDrawFile(); file.initWithLines(lineArray, new Range(0, lines.length)); return file; }// end parseFromFileContents:allowThreads: // ========== init // ============================================================== // // Purpose: Creates a new file with absolutely nothing in it. // // ============================================================================== public LDrawFile init() { super.init(); // initializes an empty list of subdirectives--in this // case, the models in the file. activeModel = null; // drawCount = 0; // editLock = [[NSConditionLock alloc] initWithCondition:0]; return this; }// end init // ========== updateModelLookupTable // ============================================ // // Purpose: Rebuilds the optimized lookup table for models. This is now // an internal method, run when we add or remove a directive, // after coder init, and any time one of our children renames // itself. // // ============================================================================== public void updateModelLookupTable() { // ArrayList<LDrawMPDModel> submodels = submodels(); // ArrayList<String> names = new ArrayList<String>(submodels.size()); nameModelDict = new Hashtable<String, LDrawMPDModel>(); for (LDrawMPDModel model : submodels()) { // always use lowercase for comparison nameModelDict.put(model.modelName().toLowerCase(), model); } } // ========== initWithLines:inRange:parentGroup: // ================================ // // Purpose: Parses the MPD models out of the lines. If lines contains a // single non-MPD model, it will be wrapped in an MPD model. // // ============================================================================== public LDrawFile initWithLines(ArrayList<String> lines, Range range, DispatchGroup parentGroup) { Range modelRange = range; int modelStartIndex = range.getLocation(); ArrayList<LDrawMPDModel> submodels = null; int insertIndex = 0; try { if (super.initWithLines(lines, range, parentGroup) != null) { submodels = new ArrayList<LDrawMPDModel>(); DispatchGroup dispatchGroup = null; do { modelRange = LDrawMPDModel .rangeOfDirectiveBeginningAtIndex(modelStartIndex, lines, range.getMaxRange()); LDrawMPDModel newModel = new LDrawMPDModel(); dispatchGroup = new DispatchGroup(); if(parentGroup!=null) dispatchGroup.extendsFromParent(parentGroup); newModel.initWithLines(lines, modelRange, dispatchGroup); // Store non-retaining, but *thread-safe* container // (NSMutableArray is NOT). Since it doesn't retain, we // mustn't // autorelease newDirective. submodels.add(insertIndex, newModel); // #if USE_BLOCKS // }); // #endif modelStartIndex = modelRange.getMaxRange()+1; insertIndex += 1; } while (modelStartIndex <= range.getMaxRange()); // #if USE_BLOCKS // dispatch_group_notify(dispatchGroup,queue, // ^{ // #endif int counter = 0; LDrawMPDModel currentModel = null; // Add all the models in order for (counter = 0; counter < insertIndex; counter++) { currentModel = submodels.get(counter); addSubmodel(currentModel); } if (submodels().size() > 0) setActiveModel(submodels().get(0)); // free(submodels); // #if USE_BLOCKS // if(parentGroup != NULL) // dispatch_group_leave(parentGroup); // // }); // dispatch_release(dispatchGroup); // #endif } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } return this; }// end initWithLines:inRange: // ========== collectColor:viewScale:parentColor: // ======================================= // // Purpose: Collect color information from ldconfig.ldr. It is used for ColorLibrary initialization. // ============================================================================== public void collectColor() { // this is like calling the non-existent method // [editLock setCondition:([editLock condition] + 1)] // [editLock lock]; //lock unconditionally // self->drawCount += 1; // [editLock unlockWithCondition:(self->drawCount)]; //don't block // multiple simultaneous draws! // // Draw! // (only the active model.) activeModel.collectColor(); // done drawing; decrement the lock's condition // [editLock lock]; // self->drawCount -= 1; // [editLock unlockWithCondition:(self->drawCount)]; }// end draw:viewScale:parentColor: // ========== drawSelf: // =========================================================== // // Purpose: Draw this directive and its subdirectives by calling APIs on // the passed in renderer, then calling drawSelf on children. // // ================================================================================ public void drawSelf(GL2 gl2, ILDrawRenderer renderer) { activeModel.drawSelf(gl2, renderer); }// end drawSelf: // ========== collectSelf: // ======================================================== // // Purpose: Collect self is called on each directive by its parents to // accumulate _mesh_ data into a display list for later drawing. // The collector protocol passed in is some object capable of // remembering the collectable data. // // Notes: The file should never be 'collected', because parts do not // reference files - rather they reference the models WITHIN // files. So while we have a release implementation of passing // the message on, we have an assert to catch this case. // // ================================================================================ public void collectSelf(ILDrawCollector renderer) { assert true : "Why are we here?"; activeModel.collectSelf(renderer); }// end collectSelf: // ========== debugDrawboundingBox // ============================================== // // Purpose: Draw a translucent visualization of our bounding box to test // bounding box caching. // // ============================================================================== public void debugDrawboundingBox(GL2 gl2) { activeModel.debugDrawboundingBox(gl2); }// end debugDrawboundingBox public void getRange( Matrix4 transform, float[] range ) { activeModel.getRange( transform, range ); } // ========== hitTest:transform:viewScale:boundsOnly:creditObject:hits: // ======= // // Purpose: Hit-test the geometry. // // ============================================================================== public void hitTest(Ray3 pckRay, Matrix4 transform, LDrawDirective creditObject, HashMap<LDrawDirective, Float> hits) { // subclasses should override this with hit-detection code activeModel.hitTest(pckRay, transform, creditObject, hits); } // ========== boxTest:transform:boundsOnly:creditObject:hits: // =================== // // Purpose: Check for intersections with screen-space geometry. // // ============================================================================== public boolean boxTest(Box2 bounds, Matrix4 transform, boolean boundsOnly, LDrawDirective creditObject, TreeSet<LDrawDirective> hits) { return activeModel.boxTest(bounds, transform, boundsOnly, creditObject, hits); }// end boxTest:transform:boundsOnly:creditObject:hits: // ========== // depthTest:inBox:transform:creditObject:bestObject:bestDepth:======= // // Purpose: depthTest finds the closest primitive (in screen space) // overlapping a given point, as well as its device coordinate // depth. // // ============================================================================== public void depthTest(Vector2f testPt, Box2 bounds, Matrix4 transform, LDrawDirective creditObject, ArrayList<LDrawDirective> bestObject, FloatBuffer bestDepth) { activeModel.depthTest(testPt, bounds, transform, creditObject, bestObject, bestDepth); }// end depthTest:inBox:transform:creditObject:bestObject:bestDepth: // ========== write // ============================================================= // // Purpose: Write out all the submodels sequentially. // // ============================================================================== public String write() { String written = new String(); String CRLF = "\r\n"; LDrawMPDModel currentModel = null; ArrayList<LDrawMPDModel> modelsInFile = submodels(); int numberModels = modelsInFile.size(); int counter = 0; // If there is only one submodel, this hardly qualifies as an MPD // document. // So write out the single model without the MPD FILE/NOFILE wrapper. if (numberModels == 1) { currentModel = modelsInFile.get(0); // Write out the model, without MPD wrappers. written += currentModel.writeModel(); } else { // Write out each MPD submodel, one after another. for (counter = 0; counter < numberModels; counter++) { currentModel = modelsInFile.get(counter); written += currentModel.write(); written += CRLF; } } // Trim off any final newline characters. return written.trim(); }// end write // ========== lockForEditing // ==================================================== // // Purpose: Aquires a mutex lock to allow safe editing. Calling this // method // will guarantee that no other thread draws or edits the file // while you are modifying it. Calls to this method must be // subsequently balanced by a call to -unlockEditor. // // If you are editing some subdirective buried deep down the file's // hierarchy, it is still your responsibility to call this method. // For performance reasons, it does NOT happen automatically! // // ============================================================================== public void lockForEditing() { // This was once a sure-fire way to hang the program if this lock // code is // enabled: // 1. Create a new model // 2. Add a part // 3. Open Inspector for that part. // 4. Click in the name field and change the part's name. DO NOT HIT // RETURN // to confirm the edit. // 5. Click back in the document. // 6. Drag the part out of the document into oblivion to delete it. // 7. Bricksmith deadlocks. This occurs because the program tries to // commit // the editing from step 4 immediately after deleting the part. Both // those operations try to aquire this lock, which they can't do // because // it isn't recursive. // // The bug here was really in step 5, in which the Inspector was // erroneously // not commiting its changes. I've fixed that bug now, so this // sequence // can't hang the application anymore. However, the warning still // stands: // this lock is not reentrant. // aquire the lock once nobody is drawing the File. The condition on // this // lock // tracks the number of threads currently drawing the File. We don't // want // to // go modifying data at the same time someone else is trying to draw // it! // [self->editLock lockWhenCondition:0]; }// end lockForEditing // ========== unlockEditor // ====================================================== // // Purpose: Releases the mutual-exclusion lock that prevents // concurrent // drawing or editing. A call to this method must be balanced by a // preceeding call to -lockForEditing. // // ============================================================================== public void unlockEditor() { // the condition tracks number of outstanding draws. We aren't a draw, // and // can't aquire this lock unless there are no draws. So we stay at 0. // [self->editLock unlockWithCondition:0]; }// end unlockEditor // // // #pragma mark - // #pragma mark ACCESSORS // #pragma mark - // // ========== activeModel // ======================================================= // // Purpose: Returns the name of the currently-active model in the // file. // // ============================================================================== public LDrawMPDModel activeModel() { return activeModel; }// end activeModel // ========== firstModel // ======================================================= // // Purpose: Returns the first model in the file, which is the one to // use // when referred to from a separate peer file. // // ============================================================================== public LDrawMPDModel firstModel() { return (LDrawMPDModel) subdirectives().get(0); }// end firstModel // ========== draggingDirectives // ================================================ // // Purpose: Returns the objects that are currently being displayed as // part // of drag-and-drop. // // ============================================================================== public ArrayList<LDrawDirective> draggingDirectives() { return activeModel().draggingDirectives(); }// end draggingDirectives // ========== modelNames // ======================================================== // // Purpose: Returns the the names of all the submodels in the file. // // ============================================================================== public ArrayList<String> modelNames() { ArrayList<LDrawMPDModel> submodels = submodels(); ArrayList<String> modelNames = new ArrayList<String>(); for(LDrawMPDModel submodel : submodels){ modelNames.add(submodel.modelName()); } return modelNames; }// end modelNames // ========== modelWithName: // ==================================================== // // Purpose: Returns the submodel with the given name, or nil if one // couldn't // be found. // // ============================================================================== public LDrawMPDModel modelWithName(String soughtName) { String referenceName = soughtName.toLowerCase();// we // standardized on lower-case names for searching. LDrawMPDModel foundModel = nameModelDict.get(referenceName); return foundModel; }// end modelWithName: // // ========== path // ============================================================== // // Purpose: Returns the filesystem path at which this file was // resides, // or // nil if that information is undetermined. Only files that are // read by the user will have their paths set; parts from the // library disregard this information. // // ============================================================================== public String path() { return filePath; }// end path // ========== submodels // ========================================================= // // Purpose: Returns an array of the LDrawModels (or more likely, the // LDrawMPDModels) which constitute this file. // // ============================================================================== public ArrayList<LDrawMPDModel> submodels() { ArrayList<LDrawMPDModel> arrayList = new ArrayList<LDrawMPDModel>(); for (LDrawDirective item : subdirectives()) arrayList.add((LDrawMPDModel) item); return arrayList; }// end submodels // // // #pragma mark - // // ========== setActiveModel: // =================================================== // // Purpose: Sets newModel to be the currently-active model in the // file. // The active model is the only one drawn. // // ============================================================================== /** * @param newModel * @uml.property name="activeModel" */ public void setActiveModel(LDrawMPDModel newModel) { NotificationCenter notificationCenter = NotificationCenter.getInstance(); if (submodels().contains(newModel)) { // Don't bother doing anything if we aren't really changing models. if (newModel != activeModel) { // Update the active model and note that something happened. activeModel = newModel; if(postsNotifications) notificationCenter.postNotification(NotificationMessageT.LDrawFileActiveModelDidChanged, null); } } else if (newModel == null) { activeModel = null; } else System.out .println("Attempted to set the active model to one which is not in the file!"); }// end setActiveModel: // ========== setDraggingDirectives: // ============================================ // // Purpose: Sets the parts which are being manipulated in the model // via // drag-and-drop. // // Notes: This is a convenience method for LDrawGLView, which might // not // care to wonder whether it's displaying a model or a file. In // either event, we just want to drag-and-drop, and that's defined // in the model. // // ============================================================================== public void setDraggingDirectives(ArrayList<LDrawDirective> directives) { activeModel().setDraggingDirectives(directives); }// end setDraggingDirectives: // ========== setEnclosingDirective: // ============================================ // // Purpose: In other containers, this method would set the object // which // encloses this one. LDrawFiles, however, are intended to be at // the root of the LDraw container hierarchy, and thus calling this // method should have no effect. // // ============================================================================== public void setEnclosingDirective(LDrawContainer newParent) { // Do Nothing. }// end setEnclosingDirective: // ========== setPath: // ========================================================== // // Purpose: Sets the filesystem path at which this file was resides. // Only // files that are read by the user will have their paths set; parts // from the library disregard this information. // // ============================================================================== public void setPath(String newPath) { this.filePath = newPath; }// end setPath: // ========== removeDirective: // ================================================== // // Purpose: In other containers, this method would set the object // which // encloses this one. LDrawFiles, however, are intended to be at // the root of the LDraw container hierarchy, and thus calling this // method should have no effect. // // ============================================================================== public void removeDirective(LDrawDirective doomedDirective) { boolean removedActiveModel = false; if(submodels().size()==1)return; if (doomedDirective == activeModel) removedActiveModel = true; super.removeObserver(doomedDirective); super.removeDirective(doomedDirective); if (removedActiveModel == true) { if (submodels().size() > 0) setActiveModel(submodels().get(0)); else setActiveModel(null); // this is probably not a good thing. } }// end removeDirective: // // #pragma mark - // #pragma mark ACTIONS // #pragma mark - // // ========== addSubmodel: // ====================================================== // // Purpose: Adds a new submodel to the file. This method only accepts // MPD // models, because adding additional submodels is meaningless // outside of MPD models. // // ============================================================================== public void addSubmodel(LDrawMPDModel newSubmodel) { insertDirective(newSubmodel, subdirectives().size()); }// end addSubmodel: // ========== insertDirective:atIndex: // ========================================== // // Purpose: Adds directive into the collection at position index. // // ============================================================================== public void insertDirective(LDrawDirective directive, int index) { super.insertDirective(directive, index); updateModelLookupTable(); // Post a notification on ourself that a model was added - missing // parts // need // to know this to re-check whether they match this model. // todo // [[NSNotificationCenter defaultCenter] // postNotificationName:LDrawMPDSubModelAdded object:self ]; }// end insertDirective:atIndex: // // ========== removeDirectiveAtIndex: // =========================================== // // Purpose: Removes the LDraw directive stored at index in this // collection. // // ============================================================================== public void removeDirectiveAtIndex(int index) { super.removeDirectiveAtIndex(index); updateModelLookupTable(); }// end removeDirectiveAtIndex: // // // #pragma mark - // #pragma mark UTILITIES // #pragma mark - // // ========== acceptsDroppedDirective: // ========================================== // // Purpose: Returns YES if this container will accept a directive // dropped // on // it. Explicitly excludes LDrawLSynthDirectives such as // INSIDE/OUTSIDE // // ============================================================================== public boolean acceptsDroppedDirective(LDrawDirective directive) { // explicitly disregard LSynth directives if (LDrawLSynthDirective.class.isInstance(directive)) { return false; } return true; } // ========== boundingBox3 // ====================================================== // // Purpose: Returns the minimum and maximum points of the box which // perfectly contains the part of this file being displayed. // // ============================================================================== public Box3 boundingBox3() { // todo // revalCache(CacheFlagBounds); Box3 ret = activeModel().boundingBox3(); return ret; }// end boundingBox3 // ========== projectedBoundingBoxWithModelView:projection:view: // ================ // // Purpose: Returns the 2D projection (you should ignore the z) of // the // object's bounds. // // ============================================================================== public Box3 projectedBoundingBoxWithModelView(Matrix4 modelView, Matrix4 projection, Box2 viewport) { return activeModel().projectedBoundingBoxWithModelView(modelView, projection, viewport); }// end projectedBoundingBoxWithModelView:projection:view: // ========== renameModel:toName: // =============================================== // // Purpose: Sets the name of the given member submodel to the new // name, // and // updates all internal references to the submodel to use the new // name as well. // // ============================================================================== public void renameModel(LDrawMPDModel submodel, String newName) { ArrayList<LDrawMPDModel> submodels = submodels(); boolean containsSubmodel = submodels.contains(submodel); String oldName = submodel.modelName(); ArrayList<LDrawPart> allParts = null; LDrawPart currentPart = null; int counter = 0; if (containsSubmodel == true && oldName != newName) { // Update the model name itself submodel.setModelName(newName); // Update all references to the old name allParts = MOCBuilder.getInstance().getAllPartInFile(); for (counter = 0; counter < allParts.size(); counter++) { currentPart = allParts.get(counter); // If the part points to the old name, change it to the new one. // Since the user can enter these values and Bricksmith is // case-insensitive, make sure to ignore case. if (currentPart.referenceName().toLowerCase().equals(oldName .toLowerCase())) { currentPart.setDisplayName(newName); } } } }// end renameModel:toName: // // // #pragma mark - // #pragma mark OBSERVATION // #pragma mark - // // // ========== receiveMessage:who: // =============================================== // // Purpose: LDrawFile overrides the message handler to get access to // name // change announcements from its contained MDP models. In this // way it can rebuild the lookup table. // // Notes: Someday if we want to get clever we could rebuild only part // of the lookup table based on the actual object that changed. // But since renames are rare it's probably not worth it. // // ============================================================================== public void receiveMessage(MessageT msg, ILDrawObservable observable) { if (msg == MessageT.MessageNameChanged) updateModelLookupTable(); super.receiveMessage(msg, observable); } // // // #pragma mark - // #pragma mark DESTRUCTOR // #pragma mark - // // //========== dealloc // =========================================================== // // // // Purpose: Takin' care o' business. // // // //============================================================================== // - (void) dealloc // { // //NSLog(@"File %s going away.\n", [filePath UTF8String]); // [nameModelDict release]; // [activeModel release]; // [filePath release]; // // [editLock release]; // // [super dealloc]; // // }//end dealloc public Vector3f rotationCenter() { assert true; return null; } // } }